package com.tianji.promotion.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.tianji.api.cache.CategoryCache;
import com.tianji.common.domain.dto.PageDTO;
import com.tianji.common.exceptions.BadRequestException;
import com.tianji.common.utils.*;
import com.tianji.promotion.constants.PromotionConstants;
import com.tianji.promotion.domain.dto.CouponFormDTO;
import com.tianji.promotion.domain.dto.CouponIssueFormDTO;
import com.tianji.promotion.domain.po.Coupon;
import com.tianji.promotion.domain.po.CouponScope;
import com.tianji.promotion.domain.po.UserCoupon;
import com.tianji.promotion.domain.query.CouponQuery;
import com.tianji.promotion.domain.vo.CouponDetailVO;
import com.tianji.promotion.domain.vo.CouponPageVO;
import com.tianji.promotion.domain.vo.CouponScopeVO;
import com.tianji.promotion.domain.vo.CouponVO;
import com.tianji.promotion.enums.CouponStatus;
import com.tianji.promotion.enums.ObtainType;
import com.tianji.promotion.enums.UserCouponStatus;
import com.tianji.promotion.mapper.CouponMapper;
import com.tianji.promotion.service.ICouponScopeService;
import com.tianji.promotion.service.ICouponService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.tianji.promotion.service.IExchangeCodeService;
import com.tianji.promotion.service.IUserCouponService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.StringRedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;

import static com.tianji.promotion.enums.CouponStatus.*;

/**
 * <p>
 * 优惠券的规则信息 服务实现类
 * </p>
 *
 * @author friday
 * @since 2024-01-14
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class CouponServiceImpl extends ServiceImpl<CouponMapper, Coupon> implements ICouponService {

    private final ICouponScopeService couponScopeService;
    private final IExchangeCodeService exchangeCodeService;
    private final IUserCouponService userCouponService;
    private final StringRedisTemplate redisTemplate;
    private final CategoryCache categoryCache;

    @Override
    @Transactional //因为操作了两个表，所以要加事务
    public void saveCoupon(CouponFormDTO dto) {
        //1.dto转po  保存优惠券 coupon表
        Coupon coupon = BeanUtils.copyBean(dto, Coupon.class);
        this.save(coupon);
        //2.判断是否限定了作用范围，dto.specific 如果为false，直接return
        if (!dto.getSpecific()) {
            return; // 没有限定了优惠券使用范围，直接返回
        }
        //3.如果限定了作用范围，校验dto.scopes是否为空，为空则返回
        List<Long> scopes = dto.getScopes();
        if (CollUtils.isEmpty(scopes)){
            throw new BadRequestException("分类id不能为空");
        }
        //4.保存优惠券的限定范围 coupon_scope表  批量新增
        /*List<CouponScope> csList = new ArrayList<>();
        for (Long scope : scopes) {
            CouponScope couponScope = new CouponScope();
            couponScope.setCouponId(coupon.getId());
            couponScope.setBizId(scope);
            couponScope.setType(1);  //目前写死，类型为 1：优惠券
            csList.add(couponScope);
        }*/
        List<CouponScope> csList = scopes
                .stream()
                .map(aLong -> new CouponScope().setCouponId(coupon.getId()).setBizId(aLong).setType(1))
                .collect(Collectors.toList());
        couponScopeService.saveBatch(csList);
    }

    //查询优惠券
    @Override
    public PageDTO<CouponPageVO> queryCouponByPage(CouponQuery query) {
        //1.分页查询优惠券表 coupon
        Page<Coupon> page = this.lambdaQuery()
                .eq(query.getType() != null, Coupon::getDiscountType, query.getType())
                .eq(query.getStatus() != null, Coupon::getStatus, query.getStatus())
                .like(StringUtils.isNotBlank(query.getName()), Coupon::getName, query.getName())
                .page(query.toMpPageDefaultSortByCreateTimeDesc());
        List<Coupon> records = page.getRecords();
        if (CollUtils.isEmpty(records)) {
            return PageDTO.empty(page.getTotal(), page.getPages());
        }
        //2.封装vo返回
        List<CouponPageVO> voList = BeanUtils.copyList(records, CouponPageVO.class);
        return PageDTO.of(page, voList);
    }

    //发放优惠券
    @Override
    public void issueCoupon(Long id,CouponIssueFormDTO dto) {
        log.debug("发放优惠券  线程名:{}",Thread.currentThread().getName());
        //1.校验
        if (id == null || !id.equals(dto.getId())){
            throw new BadRequestException("优惠券id不正确");
        }
        //2.校验优惠券id是否存在
        Coupon coupon = this.getById(id);
        if (coupon == null){
            throw new BadRequestException("优惠券id不存在");
        }
        //3.校验优惠券状态 只有待发放和暂停状态才可以发放
        if (coupon.getStatus() != CouponStatus.DRAFT && coupon.getStatus() != PAUSE){
            throw new BadRequestException("只有待发放和暂停状态才可以发放");
        }
        LocalDateTime now = LocalDateTime.now();
        //该变量代表是否可以立刻发放
        boolean isBeginIssue = dto.getIssueBeginTime()==null || !dto.getIssueBeginTime().isAfter(now);
        //4.修改优惠券的 领取开始和结束日期  使用有效期和结束日期  天数 状态

        //方式1：
        /*if (isBeginIssue) { //立刻发放
            coupon.setIssueBeginTime(now);//如果立刻发放，他设置的起始时间一定晚于当前时间，可以直接设置为当前时间
            coupon.setIssueEndTime(dto.getIssueEndTime()); //结束时间设为前端传过来的时间
            coupon.setStatus(CouponStatus.ISSUING); //设置状态为发放中
            coupon.setTermDays(dto.getTermDays()); //设置有效期天数
            coupon.setTermBeginTime(dto.getTermBeginTime()); //设置有效期开始时间
            coupon.setTermEndTime(dto.getTermEndTime());
        }else {  //不是立刻发放
            coupon.setIssueBeginTime(dto.getIssueBeginTime());
            coupon.setIssueEndTime(dto.getIssueEndTime());
            coupon.setStatus(CouponStatus.UN_ISSUE); //不是立刻发放，设置状态为未发放
            coupon.setTermDays(dto.getTermDays());
            coupon.setTermBeginTime(dto.getTermBeginTime());
            coupon.setTermEndTime(dto.getTermEndTime());
        }
        this.updateById(coupon);*/
        //方式2：
        Coupon tmp = BeanUtils.copyBean(dto, Coupon.class);
        if (isBeginIssue){
            tmp.setIssueBeginTime(now);
            tmp.setStatus(ISSUING);
        }else{
            tmp.setStatus(UN_ISSUE);
        }
        this.updateById(tmp);

        //5.如果优惠券的发放方式是立刻发放，则将优惠券信息（优惠券id、领券开始时间和结束时间、总数量、限领数量）存入redis，采用hash结构
        if(isBeginIssue){
            String key = PromotionConstants.COUPON_CACHE_KEY_PREFIX + id; //prs:coupon:优惠券id
            /*//方法1：操作了4次redis
            redisTemplate.opsForHash().put(key,"issueBeginTime", String.valueOf(DateUtils.toEpochMilli(now)));//转换为秒，方便后续取出来，比较时间
            redisTemplate.opsForHash().put(key,"issueEndTime", String.valueOf(DateUtils.toEpochMilli(dto.getIssueEndTime())));
            redisTemplate.opsForHash().put(key,"totalNum", String.valueOf(coupon.getTotalNum()));
            redisTemplate.opsForHash().put(key,"userLimit", String.valueOf(coupon.getUserLimit()));*/
            //方法2：操作一次redis
            Map<String,String> map = new HashMap<>();
            map.put("issueBeginTime", String.valueOf(DateUtils.toEpochMilli(now)));
            map.put("issueEndTime", String.valueOf(DateUtils.toEpochMilli(dto.getIssueEndTime())));
            map.put("totalNum", String.valueOf(coupon.getTotalNum()));
            map.put("userLimit", String.valueOf(coupon.getUserLimit()));
            redisTemplate.opsForHash().putAll(key,map);
        }

        //6.如果优惠券的领取方式为 指定发放 且优惠券之前的状态是待发放， 需要生成兑换码
        if (coupon.getObtainWay() == ObtainType.ISSUE && coupon.getStatus() == CouponStatus.DRAFT){
            //兑换码截止时间，就是优惠券领取的截止时间 该时间是从前端传的封装到tmp中
            coupon.setIssueEndTime(tmp.getIssueEndTime());
            //异步生成兑换码
            exchangeCodeService.asyncGenerateExchangeCode(coupon); //异步生成兑换码
        }

    }

    //查询正在发放的优惠券
    @Override
    public List<CouponVO> queryIssuingCoupons() {
        //1.查询db coupon  条件 发放中 & 手动领取
        List<Coupon> couponList = this.lambdaQuery()
                .eq(Coupon::getStatus, ISSUING)
                .eq(Coupon::getObtainWay, ObtainType.PUBLIC)
                .list();
        if (CollUtils.isEmpty(couponList)){
            return CollUtils.emptyList();
        }
        //2.查询用户券表user_coupon表  条件为当前用户  发放中的优惠券id
        //正在发放中的优惠券id集合
        Set<Long> couponIds = couponList.stream()
                .map(Coupon::getId).collect(Collectors.toSet());
        //当前用户，针对正在发放中的优惠券领取记录  1
        //1     101
        //1     101
        //1     102
        List<UserCoupon> list = userCouponService.lambdaQuery()
                .eq(UserCoupon::getUserId, UserContext.getUser())
                .in(UserCoupon::getCouponId, couponIds)
                .list();
        //101：2  102：1

        //2.1统计当前用户 针对每一个券的 已领取数量        map的键是优惠券id，值是已领取数量
        Map<Long, Long> issueMap = list.stream()
                .collect(Collectors.groupingBy(UserCoupon::getCouponId, Collectors.counting()));

        //2.2统计当前用户 针对每一个券 的已领取且未使用的数量       map的键是优惠券id，值是已领取且未使用的数量
        Map<Long, Long> unUseMap = list.stream()
                .filter(uc -> uc.getStatus() == UserCouponStatus.UNUSED)//已领取，未使用
                .collect(Collectors.groupingBy(UserCoupon::getCouponId, Collectors.counting()));

        //2 po 转 vo 返回
        List<CouponVO> voList = new ArrayList<>();
        for (Coupon c : couponList) {
            CouponVO vo = BeanUtils.copyBean(c, CouponVO.class);

            //优惠券还有剩余（issue_num < total_num）且 (统计用户券表user_coupon表中取出当前用户已领取数量 < user_limit)
            Long issNum = issueMap.getOrDefault(c.getId(), 0L); //用户已领取数量，默认为0
            boolean available = c.getIssueNum() < c.getTotalNum() && issNum.intValue() < c.getUserLimit();
            vo.setAvailable(available); //是否可以领取

            //统计用户表user_coupon表  取出当前用户已领取且未使用的券数量
            boolean received = unUseMap.getOrDefault(c.getId(),0L) > 0;
            vo.setReceived(received); //是否可以使用
            voList.add(vo);
        }
        return voList;
    }

    //根据id查询优惠券接口
    @Override
    public CouponDetailVO queryCouponById(Long id) {
        // 1.查询优惠券
        Coupon coupon = getById(id);
        // 2.转换VO
        CouponDetailVO vo = BeanUtils.copyBean(coupon, CouponDetailVO.class);
        if (vo == null || !coupon.getSpecific()) {
            // 数据不存在，或者没有限定范围，直接结束
            return vo;
        }
        // 3.查询限定范围
        List<CouponScope> scopes = couponScopeService.lambdaQuery().eq(CouponScope::getCouponId, id).list();
        if (CollUtils.isEmpty(scopes)) {
            return vo;
        }
        List<CouponScopeVO> scopeVOS = scopes.stream()
                .map(CouponScope::getBizId)
                .map(cateId -> new CouponScopeVO(cateId, categoryCache.getNameByLv3Id(cateId)))
                .collect(Collectors.toList());
        vo.setScopes(scopeVOS);
        return vo;
    }

    //暂停发放优惠券
    @Override
    @Transactional
    public void pauseIssue(Long id) {
        // 1.查询旧优惠券
        Coupon coupon = getById(id);
        if (coupon == null) {
            throw new BadRequestException("优惠券不存在");
        }
        // 2.当前券状态必须是未开始或进行中
        CouponStatus status = coupon.getStatus();
        if (status != UN_ISSUE && status != ISSUING) {
            // 状态错误，直接结束
            return;
        }
        // 3.更新状态
        boolean success = lambdaUpdate()
                .set(Coupon::getStatus, PAUSE)
                .eq(Coupon::getId, id)
                .in(Coupon::getStatus, UN_ISSUE, ISSUING)
                .update();
        if (!success) {
            // 可能是重复更新，结束
            log.error("重复暂停优惠券");
        }
        // 4.删除缓存
        redisTemplate.delete(PromotionConstants.COUPON_CACHE_KEY_PREFIX + id);
    }

    //删除优惠券
    @Override
    public void deleteById(Long id) {
        // 1.查询
        Coupon coupon = getById(id);
        if (coupon == null || coupon.getStatus() != DRAFT) {
            throw new BadRequestException("优惠券不存在或者优惠券正在使用中");
        }
        // 2.删除优惠券
        boolean success = remove(new LambdaQueryWrapper<Coupon>()
                .eq(Coupon::getId, id)
                .eq(Coupon::getStatus, DRAFT)
        );
        if (!success) {
            throw new BadRequestException("优惠券不存在或者优惠券正在使用中");
        }
        // 3.删除优惠券对应限定范围
        if(!coupon.getSpecific()){
            return;
        }
        couponScopeService.remove(new LambdaQueryWrapper<CouponScope>().eq(CouponScope::getCouponId, id));
    }

    //批量发放优惠券
    @Override
    public void beginIssueBatch(List<Coupon> coupons) {
        // 1.更新券状态
        for (Coupon c : coupons) {
            c.setStatus(CouponStatus.ISSUING);
        }
        updateBatchById(coupons);
        // 2.批量缓存
        redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
            StringRedisConnection src = (StringRedisConnection) connection;
            for (Coupon coupon : coupons) {
                // 2.1.组织数据
                Map<String, String> map = new HashMap<>(4);
                map.put("issueBeginTime", String.valueOf(DateUtils.toEpochMilli(coupon.getIssueBeginTime())));
                map.put("issueEndTime", String.valueOf(DateUtils.toEpochMilli(coupon.getIssueEndTime())));
                map.put("totalNum", String.valueOf(coupon.getTotalNum()));
                map.put("userLimit", String.valueOf(coupon.getUserLimit()));
                // 2.2.写缓存
                src.hMSet(PromotionConstants.COUPON_CACHE_KEY_PREFIX + coupon.getId(), map);
            }
            return null;
        });
    }
}
